﻿namespace MatrixRotatingWalk
{
    using System;
    using System.Text;

    public class Matrix
    {
        private static readonly int[] RowDirections = { 1, 1, 1, 0, -1, -1, -1, 0 };
        private static readonly int[] ColDirections = { 1, 0, -1, -1, -1, 0, 1, 1 };

        private int currentRow;
        private int currentCol;
        private int directionByRow;
        private int directionByCol;
        private int size;

        public Matrix(int size)
        {
            this.Size = size;
            this.IntMatrix = new int[this.Size, this.Size];
            this.CurrentRow = 0;
            this.CurrentCol = 0;
            this.DirectionByRow = RowDirections[0];
            this.DirectionByCol = ColDirections[0];
        }

        public int[,] IntMatrix { get; private set; }

        public int CurrentRow
        {
            get
            {
                return this.currentRow;
            }

            private set
            {
                if (value < 0 || value >= this.Size)
                {
                    throw new ArgumentOutOfRangeException("currentX", "CurrentX must be between 0 and matrix size!");
                }

                this.currentRow = value;
            }
        }

        public int CurrentCol
        {
            get
            {
                return this.currentCol;
            }

            private set
            {
                if (value < 0 || value >= this.Size)
                {
                    throw new ArgumentOutOfRangeException("currentY", "CurrentY must be between 0 and matrix size!");
                }

                this.currentCol = value;
            }
        }

        public int DirectionByRow
        {
            get
            {
                return this.directionByRow;
            }

            private set
            {
                if (value < -1 || value > 1)
                {
                    throw new ArgumentOutOfRangeException("directionX", "DirectionX must be only, -1,0 and 1!");
                }

                this.directionByRow = value;
            }
        }

        public int DirectionByCol
        {
            get
            {
                return this.directionByCol;
            }

            private set
            {
                if (value < -1 || value > 1)
                {
                    throw new ArgumentOutOfRangeException("directionY", "DirectionY must be only, -1,0 and 1!");
                }

                this.directionByCol = value;
            }
        }

        public int Size
        {
            get
            {
                return this.size;
            }

            private set
            {
                if (value <= 0 || value >= 100)
                {
                    throw new ArgumentOutOfRangeException("size", "Size must be between 0 and 100!");
                }

                this.size = value;
            }
        }

        public override string ToString()
        {
            StringBuilder stringBuilder = new StringBuilder();
            for (int row = 0; row < this.Size; row++)
            {
                for (int col = 0; col < this.Size; col++)
                {
                    stringBuilder.AppendFormat("{0, 4}", this.IntMatrix[row, col]);
                }

                stringBuilder.AppendLine();
            }

            return stringBuilder.ToString();
        }

        public void PrintToConsole()
        {
            Console.WriteLine(this.ToString());
        }

        public bool IsOutside()
        {
            return this.IsOutside(this.DirectionByRow, this.DirectionByCol);
        }

        public bool IsOutside(int rowDirection, int colDirection)
        {
            bool outsideRowBounds = this.CurrentRow + rowDirection < 0 || this.CurrentRow + rowDirection >= this.Size;
            bool outsideColBounds = this.CurrentCol + colDirection < 0 || this.CurrentCol + colDirection >= this.Size;

            return outsideRowBounds || outsideColBounds;
        }

        public bool IsCellEmpty()
        {
            return this.IsCellEmpty(this.DirectionByRow, this.DirectionByCol);
        }

        public bool IsCellEmpty(int rowDirection, int colDirection)
        {
            return this.IntMatrix[this.CurrentRow + rowDirection, this.CurrentCol + colDirection] == 0;
        }

        /// <summary>
        /// Check if move in current direction is available.
        /// </summary>
        /// <returns>Return true if next move is posible and false if it's blocked.</returns>
        public bool IsMoveBlocked()
        {
            return this.IsOutside() || !this.IsCellEmpty();
        }

        public void GenerateMatrix()
        {
            bool matrixFull = false;
            int cellValue = 1;
            while (!matrixFull)
            {
                this.IntMatrix[this.CurrentRow, this.CurrentCol] = cellValue;

                if (!this.HasPositionsClockwise())
                {
                    int oldRowPosition = this.CurrentRow;
                    int oldColPosition = this.CurrentCol;
                    this.LocateToFirstFreeCell();

                    if (oldRowPosition == this.CurrentRow && oldColPosition == this.CurrentCol)
                    {
                        matrixFull = true;
                        break;
                    }

                    this.ResetDirections();
                    cellValue++;
                    continue;
                }

                while (this.IsMoveBlocked())
                {
                    this.ChangeDirection();
                }

                this.CurrentRow += this.DirectionByRow;
                this.CurrentCol += this.DirectionByCol;
                cellValue++;
            }
        }

        private void LocateToFirstFreeCell()
        {
            int matrixLength = this.IntMatrix.GetLength(0);

            for (int row = 0; row < matrixLength; row++)
            {
                for (int col = 0; col < matrixLength; col++)
                {
                    if (this.IntMatrix[row, col] == 0)
                    {
                        this.CurrentRow = row;
                        this.CurrentCol = col;
                        return;
                    }
                }
            }
        }

        private void ChangeDirection()
        {
            int directionIndex = 0;
            int directionsCount = RowDirections.Length;
            for (int i = 0; i < directionsCount; i++)
            {
                if (RowDirections[i] == this.DirectionByRow && ColDirections[i] == this.DirectionByCol)
                {
                    directionIndex = i;
                    break;
                }
            }

            if (directionIndex == directionsCount - 1)
            {
                this.ResetDirections();
                return;
            }

            this.DirectionByRow = RowDirections[directionIndex + 1];
            this.DirectionByCol = ColDirections[directionIndex + 1];
        }

        private void ResetDirections()
        {
            this.DirectionByRow = RowDirections[0];
            this.DirectionByCol = ColDirections[0];
        }

        private bool HasPositionsClockwise()
        {
            int[] rowDirection = (int[])RowDirections.Clone();
            int[] colDirection = (int[])ColDirections.Clone();

            for (int i = 0; i < rowDirection.Length; i++)
            {
                if (this.CurrentRow + rowDirection[i] >= this.Size || this.CurrentRow + rowDirection[i] < 0)
                {
                    rowDirection[i] = 0;
                }

                if (this.CurrentCol + colDirection[i] >= this.Size || this.CurrentCol + colDirection[i] < 0)
                {
                    colDirection[i] = 0;
                }
            }

            for (int i = 0; i < rowDirection.Length; i++)
            {
                if (this.IntMatrix[this.CurrentRow + rowDirection[i], this.CurrentCol + colDirection[i]] == 0)
                {
                    return true;
                }
            }

            return false;
        }
    }
}
